﻿using System.ComponentModel;
using System.Timers;
using System.Windows.Media;
using System.Windows.Threading;

namespace System.Windows.Controls
{

    #region StylePresets enum

    public enum StylePresets
    {
        MacOsx,
        Firefox,
        Ie7,
        Custom
    }

    #endregion

    /// <summary>
    /// Interaction logic for ProgressWheel.xaml
    /// </summary>
    public partial class ProgressWheel : UserControl
    {
        // Constants =========================================================


        private const double NumberOfDegreesInCircle = 360;
        private const double NumberOfDegreesInHalfCircle = NumberOfDegreesInCircle/2;
        private const int DefaultInnerCircleRadius = 8;
        private const int DefaultOuterCircleRadius = 10;
        private const int DefaultNumberOfSpoke = 10;
        private const int DefaultSpokeThickness = 4;

        private const int MacOsxInnerCircleRadius = 5;
        private const int MacOsxOuterCircleRadius = 11;
        private const int MacOsxNumberOfSpoke = 12;
        private const int MacOsxSpokeThickness = 2;

        private const int FireFoxInnerCircleRadius = 6;
        private const int FireFoxOuterCircleRadius = 7;
        private const int FireFoxNumberOfSpoke = 9;
        private const int FireFoxSpokeThickness = 4;

        private const int Ie7InnerCircleRadius = 8;
        private const int Ie7OuterCircleRadius = 9;
        private const int Ie7NumberOfSpoke = 24;
        private const int Ie7SpokeThickness = 4;

        public static DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof (Color),
                                                                                     typeof (ProgressWheel),
                                                                                     new PropertyMetadata(OnColorChanged));

        public static readonly DependencyProperty OuterCircleRadiusProperty =
            DependencyProperty.Register("OuterCircleRadius", typeof (int), typeof (ProgressWheel),
                                        new PropertyMetadata(10, OnOuterCircleRadiusChanged));

        public static DependencyProperty InnerCircleRadiusProperty = DependencyProperty.Register("InnerCircleRadius",
                                                                                                 typeof (int),
                                                                                                 typeof (ProgressWheel),
                                                                                                 new PropertyMetadata(
                                                                                                     DefaultInnerCircleRadius,
                                                                                                     OnInnerCircleRadiusChanged));

        public static DependencyProperty NumberSpokesProperty = DependencyProperty.Register("NumberSpokes", typeof (int),
                                                                                            typeof (ProgressWheel),
                                                                                            new PropertyMetadata(
                                                                                                DefaultNumberOfSpoke,
                                                                                                OnNumberSpokesChanged));

        private static readonly DependencyProperty ActiveProperty = DependencyProperty.Register("Active", typeof (bool),
                                                                                                typeof (ProgressWheel),
                                                                                                new PropertyMetadata(
                                                                                                    OnActivePropertyChanged));

        public static DependencyProperty SpokeThicknessProperty = DependencyProperty.Register("SpokeThickness",
                                                                                              typeof (int),
                                                                                              typeof (ProgressWheel),
                                                                                              new PropertyMetadata(
                                                                                                  OnSpokeThicknessChanged));

        private static readonly DependencyProperty RotationSpeedProperty = DependencyProperty.Register("RotationSpeed",
                                                                                                       typeof (int),
                                                                                                       typeof (
                                                                                                           ProgressWheel
                                                                                                           ),
                                                                                                       new PropertyMetadata
                                                                                                           (20,
                                                                                                            OnRotationSpeedChanged));

        public static DependencyProperty StylePresetProperty = DependencyProperty.Register("StylePreset",
                                                                                           typeof (StylePresets),
                                                                                           typeof (ProgressWheel),
                                                                                           new PropertyMetadata(
                                                                                               StylePresets.Custom,
                                                                                               OnStylePresetChanged));

        private readonly Color _defaultColor = Colors.DarkGray;

        // Enumeration =======================================================

        // Attributes ========================================================
        private readonly Timer _timer;
        private double[] _angles;
        private Point _centerPoint;
        private Color _color;
        private Color[] _colors;
        private int _innerCircleRadius;
        private bool _isTimerActive;
        private int _numberOfSpoke;
        private int _outerCircleRadius;
        private int _progressValue;
        private int _spokeThickness;
        private StylePresets _stylePreset;

        /// <summary>
        /// Initializes a new instance of the <see cref="ProgressWheel"/> class.
        /// </summary>
        public ProgressWheel()
        {
            _color = _defaultColor;

            GenerateColorsPallet();
            GetSpokesAngles();
            GetControlCenterPoint();
            _timer = new Timer();
            _timer.Elapsed += ATimerTick;
            ActiveTimer();

            SizeChanged += ProgressWheelSizeChanged;
            _innerCircleRadius = DefaultInnerCircleRadius;
            _outerCircleRadius = DefaultOuterCircleRadius;
            _numberOfSpoke = DefaultNumberOfSpoke;
            _spokeThickness = DefaultSpokeThickness;
            _stylePreset = StylePresets.MacOsx;
        }

        /// <summary>
        /// Gets or sets the lightest color of the circle.
        /// </summary>
        /// <value>The lightest color of the circle.</value>
        [TypeConverter("System.Drawing.ColorConverter"),
         Category("ProgressWheel"),
         Description("Sets the color of spoke.")]
        public Color Color
        {
            get { return (Color) GetValue(ColorProperty); }
            set { SetValue(ColorProperty, value); }
        }

        /// <summary>
        /// Gets or sets the outer circle radius.
        /// </summary>
        /// <value>The outer circle radius.</value>
        [Description("Gets or sets the radius of outer circle."),
         Category("ProgressWheel")]
        public int OuterCircleRadius
        {
            get { return (int) GetValue(OuterCircleRadiusProperty); }
            set { SetValue(OuterCircleRadiusProperty, value); }
        }
        /// <summary>
        /// Gets or sets the inner circle radius.
        /// </summary>
        /// <value>The inner circle radius.</value>
        [Description("Gets or sets the radius of inner circle."),
         Category("ProgressWheel")]
        public int InnerCircleRadius
        {
            get { return (int) GetValue(InnerCircleRadiusProperty); }
            set { SetValue(InnerCircleRadiusProperty, value); }
        }
        /// <summary>
        /// Gets or sets the number of spoke.
        /// </summary>
        /// <value>The number of spoke.</value>
        [Description("Gets or sets the number of spoke."),
         Category("ProgressWheel")]
        public int NumberSpokes
        {
            get { return (int) GetValue(NumberSpokesProperty); }
            set { SetValue(NumberSpokesProperty, value); }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="ProgressWheel" />
        ///   is active.
        /// </summary>
        /// <value><c>true</c> if active; otherwise, <c>false</c>.</value>
        [Description("Gets or sets the number of spoke."),
         Category("ProgressWheel")]
        public bool Active
        {
            get { return (bool) GetValue(ActiveProperty); }
            set { SetValue(ActiveProperty, value); }
        }
        /// <summary>
        /// Gets or sets the spoke thickness.
        /// </summary>
        /// <value>The spoke thickness.</value>
        [Description("Gets or sets the thickness of a spoke."),
         Category("ProgressWheel")]
        public int SpokeThickness
        {
            get { return (int) GetValue(SpokeThicknessProperty); }
            set { SetValue(SpokeThicknessProperty, value); }
        }
        /// <summary>
        /// Gets or sets the rotation speed.
        /// </summary>
        /// <value>The rotation speed.</value>
        [Description("Gets or sets the rotation speed. Higher is slower."),
         Category("ProgressWheel")]
        public int RotationSpeed
        {
            get { return (int) GetValue(RotationSpeedProperty); }
            set { SetValue(RotationSpeedProperty, value); }
        }
        /// <summary>
        /// Quickly sets the style to one of these presets, or a custom style if desired
        /// </summary>
        /// <value>The style preset.</value>
        [Category("ProgressWheel"),
         Description("Quickly sets the style to one of these presets, or a custom style if desired"),
         DefaultValue(typeof (StylePresets), "Custom")]
        public StylePresets StylePreset
        {
            get { return (StylePresets) GetValue(StylePresetProperty); }
            set { SetValue(StylePresetProperty, value); }
        }

        private StylePresets Preset
        {
            set
            {
                _stylePreset = value;

                switch (_stylePreset)
                {
                    case StylePresets.MacOsx:
                        SetCircleAppearance(MacOsxNumberOfSpoke,
                                            MacOsxSpokeThickness, MacOsxInnerCircleRadius,
                                            MacOsxOuterCircleRadius);
                        break;
                    case StylePresets.Firefox:
                        SetCircleAppearance(FireFoxNumberOfSpoke,
                                            FireFoxSpokeThickness, FireFoxInnerCircleRadius,
                                            FireFoxOuterCircleRadius);
                        break;
                    case StylePresets.Ie7:
                        SetCircleAppearance(Ie7NumberOfSpoke,
                                            Ie7SpokeThickness, Ie7InnerCircleRadius,
                                            Ie7OuterCircleRadius);
                        break;
                    case StylePresets.Custom:
                        SetCircleAppearance(DefaultNumberOfSpoke,
                                            DefaultSpokeThickness,
                                            DefaultInnerCircleRadius,
                                            DefaultOuterCircleRadius);
                        break;
                }
            }
        }

        private static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // insert your code here
            var wheel = (ProgressWheel) d;
            wheel._color = (Color) e.NewValue;
            wheel.GenerateColorsPallet();
            wheel.InvalidateVisual();
        }

        private static void OnOuterCircleRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var wheel = (ProgressWheel) d;
            wheel._outerCircleRadius = (int) e.NewValue;
            wheel.InvalidateVisual();
        }

        private static void OnInnerCircleRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var wheel = (ProgressWheel) d;
            wheel._innerCircleRadius = (int) e.NewValue;
            wheel.InvalidateVisual();
        }

        private static void OnNumberSpokesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var wheel = (ProgressWheel) d;
            if ((int) e.NewValue > 0)
            {
                wheel._numberOfSpoke = (int) e.NewValue;
                wheel.GenerateColorsPallet();
                wheel.GetSpokesAngles();
                wheel.InvalidateVisual();
            }
        }

        private static void OnActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // insert your code here
            var wheel = (ProgressWheel) d;
            wheel._isTimerActive = (bool) e.NewValue;
            wheel.ActiveTimer();
        }

        private static void OnSpokeThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // insert your code here
            var wheel = (ProgressWheel) d;
            wheel._spokeThickness = (int) e.NewValue;
        }

        private static void OnRotationSpeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // insert your code here
            var wheel = (ProgressWheel) d;
            if ((int) e.NewValue > 0)
                wheel._timer.Interval = (int) e.NewValue;
        }

        private static void OnStylePresetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // insert your code here
            var wheel = (ProgressWheel) d;
            wheel.Preset = (StylePresets) e.NewValue;
        }
        /// <summary>
        /// Handles the Resize event of the LoadingCircle control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        private void ProgressWheelSizeChanged(object sender, SizeChangedEventArgs e)
        {
            GetControlCenterPoint();
        }
        /// <summary>
        /// Handles the Tick event of the aTimer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        private void ATimerTick(object sender, EventArgs e)
        {
            _progressValue = ++_progressValue%_numberOfSpoke;
            Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action) (InvalidateVisual));
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            if (_numberOfSpoke > 0)
            {
                int intPosition = _progressValue;
                for (int intCounter = 0; intCounter < _numberOfSpoke; intCounter++)
                {
                    intPosition = intPosition%_numberOfSpoke;

                    DrawLine(drawingContext,
                             GetCoordinate(_centerPoint, _innerCircleRadius, _angles[intPosition]),
                             GetCoordinate(_centerPoint, _outerCircleRadius, _angles[intPosition]),
                             _colors[intCounter], _spokeThickness);
                    intPosition++;
                }
            }
            base.OnRender(drawingContext);
        }

        /// <summary>
        /// Darkens a specified color.
        /// </summary>
        /// <param name="objColor">Color to darken.</param>
        /// <param name="intPercent">The percent of darken.</param>
        /// <returns>The new color generated.</returns>
        private static Color Darken(Color objColor, byte intPercent)
        {
            byte intRed = objColor.R;
            byte intGreen = objColor.G;
            byte intBlue = objColor.B;
            return Color.FromArgb(intPercent, Math.Min(intRed, byte.MaxValue), Math.Min(intGreen, byte.MaxValue),
                                  Math.Min(intBlue, byte.MaxValue));
        }

        /// <summary>
        /// Generates the colors pallet.
        /// </summary>
        private void GenerateColorsPallet()
        {
            _colors = GenerateColorsPallet(_color, Active, _numberOfSpoke);
        }

        /// <summary>
        /// Generates the colors pallet.
        /// </summary>
        /// <param name="objColor">Color of the lightest spoke.</param>
        /// <param name="blnShadeColor">if set to <c>true</c> the color will be shaded on X spoke.</param>
        /// <param name="numSpoke"></param>
        /// <returns>An array of color used to draw the circle.</returns>
        private Color[] GenerateColorsPallet(Color objColor, bool blnShadeColor, int numSpoke)
        {
            var objColors = new Color[NumberSpokes];

            // Value is used to simulate a gradient feel... For each spoke, the 
            // color will be darken by value in intIncrement.
            var bytIncrement = (byte) (byte.MaxValue/NumberSpokes);

            //Reset variable in case of multiple passes
            byte percentageOfDarken = 0;

            for (int intCursor = 0; intCursor < NumberSpokes; intCursor++)
            {
                if (blnShadeColor)
                {
                    if (intCursor == 0 || intCursor < NumberSpokes - numSpoke)
                        objColors[intCursor] = objColor;
                    else
                    {
                        // Increment alpha channel color
                        percentageOfDarken += bytIncrement;

                        // Ensure that we don't exceed the maximum alpha
                        // channel value (255)
                        if (percentageOfDarken > byte.MaxValue)
                            percentageOfDarken = byte.MaxValue;

                        // Determine the spoke forecolor
                        objColors[intCursor] = Darken(objColor, percentageOfDarken);
                    }
                }
                else
                    objColors[intCursor] = objColor;
            }

            return objColors;
        }

        /// <summary>
        /// Gets the control center point.
        /// </summary>
        private void GetControlCenterPoint()
        {
            _centerPoint = GetControlCenterPoint(this);
        }

        /// <summary>
        /// Gets the control center point.
        /// </summary>
        /// <returns>PointF object</returns>
        private static Point GetControlCenterPoint(UserControl objControl)
        {
            return new Point((float) objControl.Width/2, (float) objControl.Height/2 - 1);
        }

        /// <summary>
        /// Draws the line with GDI+.
        /// </summary>
        /// <param name="objGraphics">The Graphics object.</param>
        /// <param name="objPointOne">The point one.</param>
        /// <param name="objPointTwo">The point two.</param>
        /// <param name="objColor">Color of the spoke.</param>
        /// <param name="intLineThickness">The thickness of spoke.</param>
        //private void DrawLine(Graphics _objGraphics, PointF _objPointOne, PointF _objPointTwo,
        //                      System.Windows.Media.Color _objColor, int _intLineThickness)
        //{
        //    using (System.Windows.Media.Pen objPen = new System.Windows.Media.Pen(new System.Windows.Media.SolidColorBrush(_objColor), _intLineThickness))
        //    {
        //        objPen.StartLineCap = PenLineCap.Round;
        //        objPen.EndLineCap   = PenLineCap.Round;
        //        _objGraphics.DrawLine(objPen, _objPointOne, _objPointTwo);
        //    }
        //}
        private static void DrawLine(DrawingContext objGraphics, Point objPointOne, Point objPointTwo,
                                     Color objColor, int intLineThickness)
        {
            var objPen = new Pen(new SolidColorBrush(objColor), intLineThickness);
            {
                objPen.StartLineCap = PenLineCap.Round;
                objPen.EndLineCap = PenLineCap.Round;

                objGraphics.DrawLine(objPen, objPointOne, objPointTwo);
            }
        }

        /// <summary>
        /// Gets the coordinate.
        /// </summary>
        /// <param name="objCircleCenter">The Circle center.</param>
        /// <param name="intRadius">The radius.</param>
        /// <param name="dblAngle">The angle.</param>
        /// <returns></returns>
        private static Point GetCoordinate(Point objCircleCenter, int intRadius, double dblAngle)
        {
            double angle = Math.PI*dblAngle/NumberOfDegreesInHalfCircle;

            return new Point(objCircleCenter.X + intRadius*(float) Math.Cos(angle),
                             objCircleCenter.Y + intRadius*(float) Math.Sin(angle));
        }

        /// <summary>
        /// Gets the spokes angles.
        /// </summary>
        private void GetSpokesAngles()
        {
            _angles = GetSpokesAngles(NumberSpokes);
        }

        /// <summary>
        /// Gets the spoke angles.
        /// </summary>
        /// <param name="intNumberSpoke">The number spoke.</param>
        /// <returns>An array of angle.</returns>
        private static double[] GetSpokesAngles(int intNumberSpoke)
        {
            var angles = new double[intNumberSpoke];
            double dblAngle = NumberOfDegreesInCircle/intNumberSpoke;

            for (int shtCounter = 0; shtCounter < intNumberSpoke; shtCounter++)
                angles[shtCounter] = (shtCounter == 0 ? dblAngle : angles[shtCounter - 1] + dblAngle);

            return angles;
        }

        /// <summary>
        /// Actives the timer.
        /// </summary>
        private void ActiveTimer()
        {
            if (_isTimerActive)
                _timer.Start();
            else
            {
                _timer.Stop();
                _progressValue = 0;
            }

            GenerateColorsPallet();
            InvalidateVisual();
        }

        /// <summary>
        /// Sets the circle appearance.
        /// </summary>
        /// <param name="numberSpoke">The number spoke.</param>
        /// <param name="spokeThickness">The spoke thickness.</param>
        /// <param name="innerCircleRadius">The inner circle radius.</param>
        /// <param name="outerCircleRadius">The outer circle radius.</param>
        public void SetCircleAppearance(int numberSpoke, int spokeThickness,
                                        int innerCircleRadius, int outerCircleRadius)
        {
            NumberSpokes = numberSpoke;
            SpokeThickness = spokeThickness;
            InnerCircleRadius = innerCircleRadius;
            OuterCircleRadius = outerCircleRadius;

            InvalidateVisual();
        }
    }
}